Skip to content

perf(run): reads proportionate to what they show, and the case pass batched - #544

Merged
Taleef7 merged 8 commits into
mainfrom
perf/proportionate-reads-and-batched-cases
Sep 9, 2026
Merged

Taleef7 merged 8 commits into
mainfrom
perf/proportionate-reads-and-batched-cases

Conversation

@Taleef7

@Taleef7 Taleef7 commented Sep 9, 2026

Copy link
Copy Markdown
Owner

Two measurements on the live pilot stack started this, both with no run in flight.

Opening a case took 43 seconds cold and about 4 seconds warm. The route fetched every outcome row of the whole run — evidence_json blobs included — and then .find()d the one row the page renders. Roughly 87,000 rows across the wire to use one of them, growing with the roster rather than with anything the page shows.

The nightly ran at 9.5 (subject, measure) pairs a second — about three and a half hours. Outcomes were already batched; the case pass was not. Per pair it awaited a SELECT, then an INSERT or UPDATE, then an audit insert.

Reads

A read-path audit found the case-detail query copied in eight places, several worse than the original: case-actions is the detail returned by every case mutation, so assigning a case paid it too; case-outreach paid it twice per send and again on every "Preview message"; appointment-service meant the case page paid it twice per view; and routes/ai paid it before consulting its own explanation cache, so even a cache hit cost four seconds.

All eight now call one outcomeForCase helper — one row, chosen in SQL. listOutcomes gained an optional subjectId beside the measureId added in #543. limit: 1 is exactly equivalent to the .find() it replaces: both adapters order evaluated_at ASC, id ASC, so the first row under the same filter is the row .find() returned. Verified equivalent at 20k and 120k rows — same row id.

The patient profile had the same shape one level up: listCases({ limit: 100000 }), every case in the tenant, filtered in JavaScript. CaseQuery gained employeeId.

The batched case pass

CaseStore.upsertFromOutcomes takes an evaluation chunk and returns one result per input, in input order, null exactly where the single-row call returns null. Postgres reads the chunk's existing rows in one unnest join, plans in memory with the same pure planCaseUpsert/planNextAction, then writes one multi-row INSERT and one set-based UPDATE. CaseEventStore.appendAudits does the same for the ledger.

Measured against a real postgres:16, 3,000 pairs, results asserted identical to the sequential path:

round trips local wall clock
sequential 5,250 4,151 ms
batched 12 553 ms

Local has no network. At Neon's ~40 ms per round trip the case pass alone was about 140 minutes of the nightly, which is most of it.

ADR-076 d2 survives as a compare-and-set in the WHERE of the set-based UPDATE, comparing the next_action the batch read. A row an operator moved in between matches nothing and falls back to upsertFromOutcome for that row alone — the proven path, with its re-read, three attempts and action-preserving fallback. planNextAction stays the single definition of the rule; expressing it as a SQL CASE would have made the pure function dead exactly where it matters.

A duplicate key inside one batch throws in the store rather than resolving arbitrarily — a set-based UPDATE would apply one of two silently where the sequential path applied both in order. The pipeline collapses duplicates last-wins before calling, matching what it replaced, and logs a WARN so upstream duplication stays visible.

A local Postgres, which is the part that made this safe

Every store change in this repo has carried a "verified only by CI" caveat. docker compose -f infra/docker-compose.yml up -d postgres retires it: the ceiling runs 98/98 with nothing skipped.

It paid for itself immediately. Both bugs in the new SQL were Postgres-only and passed on the SQLite floor — an INSERT placeholder computed from a moving binds.length inside the row loop, and an ambiguous RETURNING once the UPDATE joined a VALUES alias carrying the same column names. The floor is a loop by design and can catch neither.

What review caught

Three reviewers; the two that completed converged independently on the same list.

CRITICAL — a real data defect. The set-based UPDATE hoisted toUpdate[0]'s runId and stamped it on every row in the chunk, while the INSERT used each row's own. Reproduced against the live database: a two-row batch with different run ids wrote the first id to both. last_run_id is the evidence pin §6.5 relies on to survive outcome compaction, and it is what countByLastRun counts — a case would have been pinned to a run that did not produce it. Latent (the pipeline passes one run id per chunk) and structurally invisible to the floor. Now a per-row column; probe passes.

The duplicate-key refusal was right in the store and wrong at the boundary. It threw inside the chunk loop, so a repeated key would fail a three-hour run outright where the per-row loop absorbed it. Reachable: the live WebChart path builds items per fetched bundle, so two bundles for one patient produce two items for one key, in the same chunk.

appendAudits was all-or-nothing per 500-row statement, so one malformed payload cost up to 500 ledger entries against the rule that every state change is audited. A failed sub-chunk now falls back to a row at a time and still propagates.

Three tests could not fail — the defect class this repo names, and my third time hitting it:

  • the "operator mid-batch" test patched before the batch, so the pre-read saw OPERATOR and the row won the CAS. The fallback path had zero coverage. Renamed to what it proves, and a real race added in the Postgres file, interposing on the unnest pre-read — the window the batch cannot see. It belongs there because the floor has no set-based UPDATE to lose.
  • the equivalence test ran against a fresh store, so every input took the INSERT path and the set-based UPDATE never executed. A new test seeds first and mixes inserts, updates and no-ops, pinning the two §4 guarantees worth pinning for a batch: IN_PROGRESS survives, a human closure is not reopened.
  • nothing crossed the 500-row sub-chunk boundary. A 1,200-row batch with every third input a no-op now does.

Two comments asserted invariants that had stopped being true, and were corrected rather than left: the progress counters advance before any case is written (they still describe persisted outcomes, which is what they always counted — the comment claimed cases), and a mid-run failure is coarser than per-row, in the recoverable direction.

A reviewability fix worth its own line

Raw NUL bytes had crept into five source files as key separators. A NUL makes git and grep classify a .ts file as binary, so it drops out of diffs and out of every code search — audit-packet.ts had been hiding that way, which is why the whole-run read inside it survived the audit that caught the other seven. Same runtime value, written \u0000. The two stores also disagreed on what a duplicate is: the floor joined keys with a space, the ceiling with NUL, so a subject id containing a space would have been refused by one and accepted by the other.

Verification

  • Postgres ceiling 98/98, 0 skipped, against this exact commit.
  • routes 397, mcp/compliance/program/export 169, run/case/audit 108, stores 118 (+1 skipped when Postgres is down).
  • pnpm typecheck clean.

Deliberately not in this PR

GET /api/runs/:id/outcomes is unbounded on the pilot profile. Bounding it means changing what X-Total-Count promises (it is the visible count, and visibility is an app-side predicate over a directory built from the rows), which is a contract change on an admin surface and belongs in its own decision.

WORKWELL_INCREMENTAL_EVAL stays off. A cache hit still persists the copied-forward outcome and still runs the case upsert, so it would have bought little while the writes dominated. It is the next lever now that they do not, and it should be turned on as a measured step rather than folded in here.

Separately, and not fixed here: the 2026-09-08 six-measure run failed because merging #543 redeployed and restarted the worker mid-run, and the orphaned row then showed RUNNING for about sixteen hours before a sweep marked it FAILED. A long run cannot survive a deploy and has no resume. This shrinks the window considerably; it does not close it.

Taleef added 7 commits September 9, 2026 10:34
Opening a case measured 43s on a cold read and ~4s warm against the pilot's six-measure nightly, with
no run in flight — so this was never load. `routes/cases.ts` fetched every outcome row of the run,
`evidence_json` blobs included, and then `.find()`d the single row the page renders. The cost grew
with the roster rather than with anything the page shows: 120,000 rows over the wire into a
single-replica worker to use one of them.

`listOutcomes` gains an optional `subjectId` alongside the `measureId` added in #543, filtered in SQL
in both stores, and the route asks for `{ subjectId, measureId, limit: 1 }`. `limit: 1` is exactly
equivalent to the old `.find()`: the store orders by `evaluated_at ASC, id ASC`, so the first row
under the same filter is the row `.find()` would have returned. Both columns are index-friendly —
`outcomes` carries `(subject_id, measure_id, evaluation_period)` and `(run_id)`.

The store-contract test pins the composition of the two filters and asserts the result is the same row
the unfiltered `.find()` picked, so an implementation that narrowed differently fails rather than
quietly returning a neighbour.
… definition

The whole-run read fixed in the previous commit was written out eight times. A read-path audit found
it at case-actions (the detail returned by EVERY case mutation — assign, escalate, resolve, priority),
case-outreach (twice per send: renderContext then buildDetail, and again on every "Preview message"),
appointment-service (so the case detail page paid it TWICE per view, once for the case and once for
its appointments), routes/ai (before the explanation cache is consulted, so even a cache hit paid it),
case-rerun, both MCP case tools, and the auditor case packet.

All now call `outcomeForCase(outcomes, lastRunId, subjectId, measureId)` — one row, chosen in SQL.
A shared helper rather than eight edits, so the ninth copy has somewhere to go instead.

`audit-packet.ts` was invisible to the grep that found the others: it contains two literal NUL bytes
as a composite-key joiner, so grep classifies it as binary and skips it. Intentional and committed,
but worth knowing it hides from search.
…he whole tenant

The patient profile — the page a quality lead opens to see one person's quality — read
`listCases({ limit: 100000 })`, every case in the tenant, and kept the ones whose employeeId matched.
It scaled with the practice's case count rather than with the patient being looked at.

`CaseQuery` gains `employeeId`, filtered in SQL on both stores. The contract test pins that it
composes with `measureId` rather than replacing it, and that omitting it does not filter — a filter
that silently won the other would go unnoticed at demo scale and only bite the pilot.
The nightly evaluates 120,000 (subject, measure) pairs and the case pass awaited two round trips per
pair — a SELECT then an INSERT or UPDATE — plus a third for each audit event. Measured on the pilot at
9.5 pairs a second, about three and a half hours; the run of 2026-09-08 was killed by a deploy at
87,000 and then showed RUNNING for sixteen hours. Outcomes were already batched; the case pass was not.

`CaseStore.upsertFromOutcomes` takes a chunk and returns one result per input, in input order, null
exactly where the single-row call returns null. Postgres reads every existing row for the chunk's keys
in one `unnest` join, plans in memory with the same pure `planCaseUpsert`/`planNextAction`, then writes
one multi-row INSERT and one set-based UPDATE. `CaseEventStore.appendAudits` does the same for the
ledger.

Measured against a real postgres:16, 3,000 pairs, results asserted identical to the sequential path:
5,250 round trips to 12, and 4,151ms to 553ms locally — where a local socket pays none of the ~40ms
Neon costs per trip. At that RTT the case pass alone was ~140 minutes of pure latency per nightly.

ADR-076 d2 survives as a compare-and-set in the WHERE of the set-based UPDATE, comparing the
`next_action` we READ. A row an operator moved in between matches nothing and falls back to
`upsertFromOutcome` for that row alone — the proven path, with its re-read, its three attempts and its
action-preserving fallback. `planNextAction` stays the single definition of the rule; expressing it as
a SQL CASE would make the pure function dead where it matters. Same for a key another writer inserted
first.

A duplicate key inside one batch THROWS rather than resolving arbitrarily: a set-based UPDATE would
apply one of the two silently where the sequential path applied both in order.

The SQLite floor is a loop, and says so — the batching buys round trips, and a local file has none.
That means every batch-shaped contract test passes on the floor without exercising set-based SQL, so
the Postgres ceiling is where this is really tested. Both failures found while writing it were
Postgres-only: an INSERT placeholder computed from a moving `binds.length` inside the row loop, and an
ambiguous `RETURNING` once the UPDATE joined a VALUES alias carrying the same column names.

The audit batch is awaited through `Promise.resolve().then(...)`, not a bare `.catch()` on the call: a
`.catch()` handles a rejection, but a synchronous throw escapes it and would take the cycle rollover
and the terminal event down with it. The old per-row call had the same latent hole.
… fails the run, and tests that can fail

Three reviewers, two of them independent, converged on the same list.

CRITICAL, and a real data defect: the set-based UPDATE hoisted `toUpdate[0]`'s `runId` and stamped it
on every row in the chunk, while the INSERT path used the row's own. Reproduced against a live
postgres:16 — a two-row batch with different run ids wrote the first id to both. `last_run_id` is the
evidence pin §6.5 relies on to survive outcome compaction, and it is what `countByLastRun` counts, so a
case would have been pinned to a run that did not produce it. The pipeline passes one run id per chunk
today, so it was latent — and invisible to the SQLite floor, which loops and is correct by
construction. `last_run_id` is now a per-row column in the VALUES list, and the probe passes.

The duplicate-key refusal was correct in the store and wrong at the boundary: it threw inside the chunk
loop, so a repeated key would fail a three-hour run outright where the per-row loop had absorbed it
last-wins. A repeat is reachable — the live WebChart path builds items per fetched bundle, so two
bundles for one patient produce two items for one key, in the same chunk. The pipeline now collapses
duplicates last-wins, matching what it replaced, and logs a WARN so the upstream duplication is still
visible. The store keeps the throw as the backstop.

`appendAudits` was all-or-nothing per 500-row statement, so one malformed payload cost up to 500 ledger
entries against the rule that every state change is audited. A failed sub-chunk now falls back to a row
at a time — the same "losers take the proven path" shape the case store uses — and still propagates so
the caller hears that the ledger is incomplete.

Tests that could not fail, which is the defect class this repo names and I have now hit three times:
- the "operator mid-batch" contract test patched BEFORE the batch, so the pre-read saw OPERATOR and the
  row WON the compare-and-set. It never reached the fallback. Renamed to what it actually proves, and a
  real race added in the Postgres file — it interposes on the `unnest` pre-read, which is the window
  the batch cannot see. It belongs there because the floor has no set-based UPDATE to lose.
- the equivalence test ran against a fresh store, so every input took the INSERT path and the set-based
  UPDATE never executed. A new test seeds first and mixes inserts, updates and no-ops in one batch,
  and pins the two §4 guarantees worth pinning for a batch: IN_PROGRESS survives, a human closure is not
  reopened.
- nothing crossed the 500-row sub-chunk boundary. A 1,200-row batch with every third input a no-op now
  does, so a shifted index shows up as a null in the wrong slot.

Two comments asserted invariants that had stopped being true: the progress counters advance before any
case is written now (they still describe persisted OUTCOMES, which is what they always counted — the
comment claimed cases), and a mid-run failure is coarser than per-row, in the recoverable direction.

Also: raw NUL bytes had crept into five source files as key separators. A NUL makes git and grep treat
a `.ts` file as binary, so the file drops out of diffs and out of every search — `audit-packet.ts` had
been hiding from code search this way for months, which is why a whole-run read in it survived. Same
runtime value, written as `\u0000`. Both stores now join duplicate keys identically; the floor had a
space, so a subject id containing a space would have been refused by one store and accepted by the other.
@Taleef7

Taleef7 commented Sep 9, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@Taleef7 Taleef7 self-assigned this Sep 9, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ad101a107a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +399 to +401
WHERE c.employee_id = v.employee_id AND c.measure_id = v.measure_id AND c.evaluation_period = v.evaluation_period
AND c.next_action IS NOT DISTINCT FROM v.expected_next_action
AND c.next_action_source IS NOT DISTINCT FROM v.expected_next_action_source

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Recheck case status before applying batched updates

When this batch has pre-read an OPEN case and scheduleAppointment concurrently changes only its status to IN_PROGRESS, these predicates still match because that action does not modify either next_action field. The update then writes the stale planned status (OPEN) and silently undoes the scheduling state, violating the state-aware upsert contract that preserves IN_PROGRESS. Include the status-related planning fields in the compare-and-set or re-read/lock the row before applying the update.

Useful? React with 👍 / 👎.

…as made from

Codex P2 on #544, and it is right. The CAS compared `next_action` and `next_action_source` only, but
`planCaseUpsert` reads `status`, `current_outcome_status` and `closed_by`, and `planNextAction` reads
the two action columns plus `current_outcome_status` — five inputs, two guarded.

So a concurrent write touching neither action column slipped through. `scheduleAppointment` is exactly
that: `patchCase(caseId, { status: "IN_PROGRESS" })` and nothing else. The batch's planned
`status: "OPEN"` then landed on top of it and silently undid the scheduling, against §4's most-cited
guarantee.

The single-row path guards the same two columns and has the same hole, but its read-to-write window is
microseconds; a batch plans a whole chunk and issues an INSERT before its UPDATE, so the window is
orders of magnitude wider. The batch is now strictly stricter than the per-row path, which is the right
asymmetry: a row that fails the wider guard falls back to `upsertFromOutcome`, which re-reads, re-plans
against the row as it now is, and gets the correct answer.

The new Postgres test interposes on the `unnest` pre-read and patches only the status, reproducing the
exact scenario. Verified non-vacuous: with the three added predicates removed it fails on
"the operator's IN_PROGRESS survived the batch's stale plan", and passes with them.

Fixing it also caught a cast/column misalignment of my own — the three new expected_* values are pushed
before `runId`, so `::uuid` had to move from position 14 to 17. The VALUES cast list now carries a
numbered comment mapping every position, because this is the second time an off-by-one there has cost a
debugging round.
@Taleef7

Taleef7 commented Sep 9, 2026

Copy link
Copy Markdown
Owner Author

Fixed in db8489d — you were right, and the mechanism is exactly as described.

planCaseUpsert reads status, current_outcome_status and closed_by; planNextAction reads the two action columns plus current_outcome_status. Five plan inputs, two guarded. scheduleAppointment patches { status: "IN_PROGRESS" } and nothing else, so it slipped through the guard and the batch's planned OPEN landed on top of it.

The compare-and-set now covers all five — "only write if the row is still the row I planned from". A row that fails it falls back to the per-row upsertFromOutcome, which re-reads and re-plans against the row as it now is.

Worth noting the single-row path has the same two-column guard and the same hole; what made it worth closing here is that a batch plans a whole chunk and issues an INSERT before its UPDATE, so the read-to-write window goes from microseconds to hundreds of milliseconds. The batch is now strictly stricter than the per-row path.

Added a Postgres test that interposes on the unnest pre-read and patches only the status. Verified non-vacuous: with the three added predicates removed it fails on "the operator's IN_PROGRESS survived the batch's stale plan"; with them it passes. Ceiling is 99/99, 0 skipped.

@Taleef7
Taleef7 merged commit 2fdf2f5 into main Sep 9, 2026
33 checks passed
@Taleef7
Taleef7 deleted the perf/proportionate-reads-and-batched-cases branch September 9, 2026 16:47
Taleef7 added a commit that referenced this pull request Sep 17, 2026
…the measurement-year report (#574)

* feat(lists): the ACO's attributed list is an immutable, revisioned table on both stores

The one concrete ask from the 2026-09-09 working session: hand WorkWell the list
of patients the ACO attributes to the group, run the measures over exactly that
subset, and get numerator/denominator/exclusions back with patient-level results.
Every other population question this deployment answers is "the patients in our
directory"; this one is "the patients somebody else says are ours".

Two tables, owner-authorized in session on 2026-09-15. IMMUTABLE is the design
rather than a nicety: a report is a function of (list revision, run ids), so a
list that could be edited underneath one would make every number the ACO had
already filed unverifiable. A re-import is a new row with revision+1 under the
same name, the store interface exposes no mutator, and a manual resolution of a
non-match is therefore also a new revision — the rule ADR-022 applies to identity
matching.

NOT_FOUND members are KEPT. An identifier the directory cannot resolve is the ACO
and the practice disagreeing about who a patient is, which is a finding for the
review queue; dropping it would remove the patient from the denominator and from
the evidence that they were ever claimed. `resolution` and `subject_id` are
coupled by a CHECK rather than by convention, and a partial unique index refuses
a second MATCHED row for one subject, so an alias lands AMBIGUOUS rather than
doubling a patient in every denominator the list feeds.

Nothing is visible until the whole import landed — header IMPORTING, members in
chunks, audit, then COMPLETE, with every read filtering COMPLETE. "One
transaction" was never available on the floor (D1 caps a batch at ~90
statements), so rather than let the ceiling be quietly safer, both backends rely
on the flip and a crash at any point leaves an invisible partial row instead of a
short list masquerading as whole. The revision allocation IS transactional,
because it is the only part with a real race: an advisory xact lock on the
ceiling (never the session form — Neon's pooler is PgBouncer in transaction
mode), a single INSERT…SELECT MAX+1 with one retry on the floor.

The import rules are pure and tested where they can fail. Two are invisible from
a route test: resolution never consults a lookup that FABRICATES profiles (the
live directory returns a minimal profile for any `wc|` string, so a resolver
built on it would auto-match identifiers that exist nowhere), and the alias
collapse cannot fire under today's exact-id matching but is the seam the
identifier format changes at.

The sandbox data boundary is per PROFILE, and writing it as one pattern was the
first thing that would have been wrong twice: the Maui corpus is 48 fixture
patients with three-digit ids followed by generated five-digit ones, so a gate
written for the generated form alone would refuse the first 48 real patients in
the sandbox; and the default deployment is a different synthetic roster
(emp-NNN), where the corpus pattern would refuse everything. It is a namespace
test, not an existence test — pat-99999 conforms and is simply NOT_FOUND, so the
review queue is exercised with synthetic-shaped ids.

Verification: 12 store-contract tests green on the SQLite floor; 13 import-rule
tests green. The Postgres ceiling contract runs in CI's postgres:16 service —
Docker is down on this host and the floor cannot catch Pg-only SQL, which is how
two defects reached #544.

* feat(lists): import an attributed list, and filter every subject surface by it

The import route, its sandbox data boundary, and `?listId=` through the one
predicate six surfaces already share.

EVERY method on /api/subject-lists is CASE_MANAGER/ADMIN, metadata included —
deliberately not split into a GET rule and a write rule the way panels is. A
member row is a raw patient identifier another system asserted, and the list's
mere existence says which patients an ACO claims. The public /sandbox signs in as
a read-only VIEWER that may browse every AUTHENTICATED GET, and on Maui the
clinician seat is a VIEWER too, so leaving the reads to the /api/** catch-all
would have handed all of it to anyone who pressed "explore the sandbox".

The sandbox data boundary refuses the WHOLE upload before persistence when any
identifier is outside this deployment's own namespace, and reports the COUNT
rather than the values — an error body is logged and kept by the browser, so
echoing them would persist the very data the gate refuses. A live-directory
deployment cannot import at all: that directory is a worker-local last-known
registry that also fabricates profiles for `wc|` ids, so matching against it
would be silently incomplete — every row MATCHED and every denominator wrong —
rather than merely unavailable. The gate runs before the body is read.

The audit payload carries counts, provenance and revision and NO identifier; a
test asserts it, because audit_events is exported wholesale and an identifier
there would defeat the namespace gate by another route.

`?listId=` is a RESOLVED membership, never a token the predicate parses: the
parameter names an immutable list and the server turns it into subjects, so a
client cannot spell one. The set is memoized in an 8-entry LRU, which is safe
only because a list is immutable — a 50,000-id Set rebuilt on every /worklist
page load is the pool-pressure shape #560 removed from the case export.
`hasActiveSubjectFilters` tests `!= null`, not `.size > 0`: a list none of whose
identifiers resolved is a legitimate answer and an ACTIVE filter matching nobody,
and reading it as absent would answer "the ACO's population" with everybody.

Verification: 12 route tests, 2 Maui-profile gate tests (each profile's own
namespace is the admitted one — a single pattern would have refused the first 48
corpus patients on one profile and everything on the other), 6 cross-surface
tests, 1 authorization test; 108 existing route tests still green. Two mutations,
each caught: `.size > 0` in the active-filter guard revives the empty-list hole,
and dropping the resolution from the cases CSV breaks all three surface tests.

* feat(report): the attributed list's measurement-year numbers, with patient-level evidence

What the ACO actually asked for on 2026-09-09: numerator, denominator and
exclusions over the patients they attribute to the group, with the per-patient
result and its date. Computed from the same evidence every other rate here comes
from — `createRateAggregator` over the outcomes' persisted population
memberships — fed only the rows whose subject is in the list.

Four things that were each a way to be quietly wrong:

`measurementYear` is REQUIRED and has no default. An officially routed run is
scored over the calendar year containing its evaluation date (ADR-072), so in
January 2028 the newest run is a PY2028 one and "the latest numbers" would answer
a PY2027 question with next year's first nightly — a wrong number that looks
exactly like a right one. The selector walks past a newer run from the wrong
year.

Compaction is checked PER MEASURE, before and after the reads. ADR-077 refuses a
report built over rows that may be incomplete, but the refusal belongs to the
measure whose run aged out; withholding five complete measures because the sixth
did would be a second wrong answer. The whole request is 409 only when every
selected run is exposed. The post-read check is what makes a mid-report
compaction pass a 409 rather than a truncated 200 — a streamed CSV cannot change
its status after the first byte, so every derived row is computed before anything
is serialised.

`missingFromRun` is reported BESIDE the rates and never subtracted. A member the
run never evaluated is a gap in the evidence, not an exclusion, and folding them
into a denominator would let a SMALLER run produce a HIGHER score. Two
reconciliations are pinned by test: matched = seen + missing, and seen = scored +
unmeasured + errors + outOfPopulation.

The CSV is the patient-level artifact, with its header pinned exactly because the
ACO's tooling reads it by name. Three row shapes: one per (measure, rate) when
evaluated; one per MEASURE for a member the run never saw, with every population
cell EMPTY rather than 0 (which reads as a scored result); and exactly ONE row
per unresolved identifier, after the evaluated rows — one per measure would
multiply one disagreement by six. Text a person supplied is neutralised against
spreadsheet formula injection, because a CSV of somebody's uploaded identifiers
must not become code when the ACO opens it. `subjectHeaders` is exported rather
than copied, so the subject columns follow the deployment's own term.

The report read is audited like COMPLIANCE_API_READ, naming the list, revision
and run ids — what the December question about March's numbers needs — and no
identifier.

Verification: 14 report tests, 5 new route tests, 17 route tests in total for the
surface. Backend 2,724 tests, 2,700 pass, one failure — the standing
corpus-membership stale sparse-checkout one, green in CI. Two mutations, each
caught: dropping the post-read compaction check fails 3 tests, and taking the
newest run regardless of year fails 5.

* feat(lists): the attributed-list screen — import, the review queue, and the year's numbers

One page for the ask: upload the list, see what resolved and what did not, and
take the measurement year's numbers off it.

The unresolved members get a filter of their own rather than a count in a corner.
An identifier the directory cannot resolve is the ACO and the practice
disagreeing about who a patient is — a finding somebody has to work, and a list
that reported only its matched count would hide exactly the rows worth a phone
call.

The server's refusal is rendered VERBATIM. The sandbox-namespace gate answers
with a COUNT of identifiers outside the deployment's directory, and "Import
refused" alone would drop the one number the operator needs to go back to the
ACO with.

The report's measurement year is a select with no default, because ADR-072 scores
an officially routed run over its calendar year: "the latest numbers" would
answer a PY2027 question with PY2028's first nightly the moment January arrives,
and it would look right. A measure whose run predates a retention cutoff is named
with its reason beside the measures that did report — withholding the others
would be a second wrong answer, and showing that one's numbers would be the
first.

The lists hook carries the same hardening `use-panel-payers` and
`use-assignable-users` needed: a row that is not what it claims to be is dropped,
and a payload that arrived wholly unusable warns rather than rendering an empty
state that reads as "this deployment has no lists". Both fetch effects defer out
of the synchronous body, the idiom compliance/ and cases/ already use for
react-hooks/set-state-in-effect; the members table resets its page in the CHANGE
HANDLER and remounts per list via a key, so no effect writes state at all.

Verification: 5 page tests, frontend lint clean, 471 tests, build compiled. One
mutation, caught: dropping the row shape guard fails the picker test.

* docs(lists): ADR-082, the report's column contract, and the two PHI-phase items it defers

ADR-082 records seven decisions: a list is immutable (a report is a function of
list revision and run ids, so an editable list makes every filed number
unverifiable); an attribution is not an assignment and neither is a denominator;
NOT_FOUND members are kept and an alias is AMBIGUOUS rather than a silent
collapse; the sandbox data boundary, per profile and before persistence; the
report is for a measurement year and compaction refuses per measure;
missingFromRun is reported beside the rates and never subtracted; and the CSV is
the patient-level artifact with a pinned header.

DATA_MODEL_CONTRACTS gains §6.6 (the report's columns, its three row shapes, and
why the two non-evaluated ones carry the empty string rather than 0) and §6.7
(`?listId=` on the six filtered surfaces). Both are APPENDED — nothing in
§6.1–§6.5 moves.

DATA_MODEL §3.29 carries the two tables and the visible-state machine; MCP.md
documents the listId argument and states that it exposes nothing the tool's role
gate did not; ARCHITECTURE §3 places the report as the sixth population read
model and the only one whose population is asserted from outside; DEPLOY gains
the runbook — nothing to run, no backfill, rollback-safe, the request budgets,
and the retained-report stopgap that is the ACO's audit trail until the archive
ships.

PRODUCTION_READINESS §4 gains the two PHI-phase items this defers: an
authoritative subject resolver (the live directory fabricates profiles for
unknown `wc|` ids, so matching against it would be silently incomplete) and the
per-run report archive, with its design, for the ACO's audit window.

Guide chapter 10 gains the scenario in plain words, and the journal entry records
the two source hazards this session hit: a literal U+FEFF and a literal tab + CR
written into regexes by tooling that collapsed the escapes, both invisible on
screen and caught only by dumping bytes.

* test(e2e): the attributed-list screen and its authorization boundary, read-only

Importing is a WRITE and belongs in `maui-writes`; this project runs first and
must be harmless when the thing it tests is broken, so every assertion is about a
surface that exists whether or not any list has been imported, and the negative
checks name a list id no import can have produced.

The one thing worth an e2e rather than a unit test: this route's READS are
CM/ADMIN, unlike every other directory surface on the deployment. A regression
that dropped them to the AUTHENTICATED catch-all would look entirely normal in a
unit test of the page, and would hand an ACO's attribution to the read-only
sandbox seat.

* fix(lists): the review round — a gate that could not fire, and a reconciliation that was false

Four review lanes on the whole diff. The two that matter were found
independently by more than one of them.

`?listId=` re-opened the membership the CM/ADMIN gate exists to close. Every
method on /api/subject-lists is CM/ADMIN because the list's existence says which
patients an ACO claims — but five of the six surfaces that accept `?listId=` are
AUTHENTICATED, so a read-only VIEWER holding a list id could take the whole
membership out of `GET /api/exports/cases?format=csv&listId=<uuid>`: names,
provider, payer, per-measure status. That is strictly MORE than the members
endpoint the gate protects, and the id is not a secret by construction — it sits
in the query string of every filtered screen, so it reaches shareable URLs,
browser history and access logs. A gate that reads as present and cannot fire for
the widest read is this repo's own vacuous-guard shape at the system level.
Enforced once in the worker, where authorization decisions live.

The stated reconciliation was arithmetically false. `createRateAggregator`'s
`unmeasured` is a SUPERSET of its `evaluationErrors` — it starts the count at the
error count — so subtracting both double-counted every error, and a row that was
both out-of-population and an evaluation error made `scoredSubjects` NEGATIVE.
The test that "pinned" it used a fixture with all three counts at zero, which
makes the assertion `2 === 2 + 0 + 0 + 0` and passes for any implementation. Each
seen subject is now classified into exactly ONE bucket in a stated order (error,
then out-of-population, then in-no-rate, then scored), so the identity holds on a
PARTIAL_FAILURE run — an ordinary night on the pilot.

Also, each verified before folding:

- The default profile's namespace refused fifty legitimate `ihn-emp-NNN` members
  of its own directory — the same defect as refusing Maui's 48 fixtures, in the
  other deployment. And an unrecognised profile is refused outright now rather
  than lent another profile's namespace.
- The run search took the twelve most recent runs outright, which on a nightly
  deployment is twelve DAYS: from mid-January a report for the closed year
  answered "no run" while that year's runs sat uncompacted.
  `listLatestPopulationRuns` walks at most 25 runs whatever count it is given, so
  scoping to the year's window was the only mechanism that could work.
- The per-row rate flags built a whole aggregator per row — 300,000 of them at
  the 50,000-member cap across six measures — where `membershipRatesFor` is the
  thing wanted, and it is read once per row for both the flags and the bucket.
- `name`/`source`/`note` were the one unvalidated channel into Neon, and
  name/source are copied into an audit payload that is exported wholesale. Capped
  at 200 by the route and by a CHECK in both schemas.
- `listMembers` and `countMembers` did not filter COMPLETE, safe only because
  every caller checked first — a claim about callers, not about the store.
- The duplicate collapse compared timestamps as STRINGS, so an offset-form
  `evaluatedAt` could pick the older clinical evaluation.
- The CSV carried the resolved subject id in the `rawIdentifier` column, which is
  right only while matching is exact and wrong the moment the format changes —
  which is the entire point of that column.
- `emptyEntry` broke `matchedSubjects = seen + missingFromRun` for exactly the
  entries a reader is most likely to check: the ones with no numbers.
- The 2 MB cap was measured after buffering the body, and had no test.
- A test titled "the body is never read" asserted only a status code.

One claim was checked and REJECTED: a lane reported that an unrecognised profile
would fail OPEN and admit real identifiers. It would not — the fallback was the
`emp-` pattern, which refuses an MRN — and the profile id is a closed union. The
underlying point was still worth taking.

Verification: backend typecheck clean, 2,738 tests, one failure (the standing
corpus-membership stale sparse-checkout, green in CI). Seven mutations, each
caught; the new one is removing the `?listId=` gate, which fails three worker
tests.

* fix(report): select a run by the period it SCORES, and emit rows for a measure that never ran

Four findings from Codex on the open PR, two of them P1.

The run selection filtered candidates by when a run STARTED. A manual run takes
an arbitrary `evaluationDate`, so a rerun-to-verify of a closed year begins in
the following one and legitimately scores the closed one — I had written that
limit into ADR-082 as acceptable, and it is not, because a backdated rerun is a
supported path rather than a hypothetical. The report would answer
`no_completed_population_run_for_year` with that run sitting in the table, which
is the shape of wrong answer this project refuses: it looks exactly like a right
one. `RunStore.listPopulationRunsForPeriod` filters on the run's own
`measurement_period_start`, which also removes the 25-run walk cap that made the
start-date window necessary in the first place, and is simpler than what it
replaced.

A measure with no usable run emitted no patient rows while its summary claimed N
members were missing from it. The CSV serialises `rows` alone, so the count was
unreconstructable from the artifact and the ACO could not see WHO. It emits one
MISSING_FROM_RUN row per matched member now. A COMPACTED measure stays the
exception and claims nothing per subject — no rows, `missingFromRun: 0` —
because ADR-077 refuses numbers built over rows that may be incomplete, and "how
many of your patients did this measure miss?" is such a number.

On the screen: the year select offered only past years, which made PY2027 — the
year the pilot exists for — unreachable until the clock caught up, even though a
run can already be created with that evaluation date. And changing the year left
the computed table and the Download button up while `download` read the NEW year,
so an operator could read one year's numbers and download another's; an in-flight
compute could also land after a year change and repopulate the stale table.

Both selects on the page are native `<select>` elements with `aria-label` now,
following the roster's page-size control: `@mieweb/ui`'s Select renders a custom
combobox whose options are not in the DOM, so `userEvent.selectOptions` cannot
drive it, and a control with behaviour worth pinning has to be drivable by a
test. Two sibling components were also keyed by the same list id.

Verification: backend typecheck clean, 2,742 tests, one failure (the standing
corpus-membership stale sparse-checkout, green in CI); frontend lint clean, 473
tests, build compiled. A new store-contract test proves the period read selects a
run started in a LATER year, and refuses a CASE-scope rerun and an unfinished run.

---------

Co-authored-by: Taleef <taleef@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant